
/*
 *  Copyright (C) 2010-2013 Alibaba Group Holding Limited
 */


#include <ngx_config.h>
#include <ngx_core.h>
#include <ngx_http.h>


#define NGX_HTTP_TRIM_FLAG "http_trim"

#define NGX_HTTP_TRIM_SAVE_SLASH      -1
#define NGX_HTTP_TRIM_SAVE_JSCSS      -2
#define NGX_HTTP_TRIM_SAVE_SPACE      -3
#define NGX_HTTP_TRIM_SAVE_HACKCSS    -4
#define NGX_HTTP_TRIM_SAVE_JAVASCRIPT -5

#define NGX_HTTP_TRIM_TAG_PRE      1
#define NGX_HTTP_TRIM_TAG_STYLE    2
#define NGX_HTTP_TRIM_TAG_SCRIPT   3
#define NGX_HTTP_TRIM_TAG_TEXTAREA 4

typedef struct
{
    ngx_hash_t   types;
    ngx_array_t *types_keys;

    ngx_http_complex_value_t *js;
    ngx_http_complex_value_t *css;
    ngx_http_complex_value_t *trim;
} ngx_http_trim_loc_conf_t;


typedef struct
{
    u_char prev;

    ngx_chain_t *in;
    ngx_chain_t *free;
    ngx_chain_t *busy;

    size_t looked;
    size_t saved_comment;

    off_t tin;
    off_t tout;

    ngx_int_t  tag;
    ngx_int_t  saved;
    ngx_int_t  count;
    ngx_uint_t state;

    unsigned js_enable  : 1;
    unsigned css_enable : 1;
} ngx_http_trim_ctx_t;


typedef enum
{
    trim_state_text = 0,
    trim_state_text_whitespace, /* \r \t ' ' */
    trim_state_tag,             /* <  */
    trim_state_tag_text,
    trim_state_tag_attribute,
    trim_state_tag_whitespace,   /* \r \n \t ' ' */
    trim_state_tag_single_quote, /* '  */
    trim_state_tag_double_quote, /* "  */
    trim_state_tag_s,            /* <s */
    trim_state_tag_pre_begin,    /* <pre */
    trim_state_tag_pre,
    trim_state_tag_pre_angle,
    trim_state_tag_pre_nest,
    trim_state_tag_pre_end,        /* <pre    </pre> */
    trim_state_tag_textarea_begin, /* <textarea */
    trim_state_tag_textarea_end,   /* <textarea </textarea> */
    trim_state_tag_style_begin,    /* <style */
    trim_state_tag_style_end,      /* <style    </style> */
    trim_state_tag_style_css_end,  /* <style    </style> */
    trim_state_tag_style_css_text, /* <style type="text/css" */
    trim_state_tag_style_css_whitespace,
    trim_state_tag_style_css_single_quote,
    trim_state_tag_style_css_single_quote_esc,
    trim_state_tag_style_css_double_quote,
    trim_state_tag_style_css_double_quote_esc,
    trim_state_tag_style_css_comment,
    trim_state_tag_style_css_comment_begin,
    trim_state_tag_style_css_comment_end,
    trim_state_tag_style_css_comment_begin_empty,
    trim_state_tag_style_css_comment_empty,
    trim_state_tag_style_css_comment_begin_hack,
    trim_state_tag_style_css_comment_hack,
    trim_state_tag_style_css_comment_hack_text,
    trim_state_tag_style_css_comment_hack_text_begin,
    trim_state_tag_style_css_comment_hack_text_end,
    trim_state_tag_style_css_comment_hack_text_last,
    trim_state_tag_script_begin,   /* <script */
    trim_state_tag_script_end,     /* <script   </script> */
    trim_state_tag_script_js_end,  /* <script   </script> */
    trim_state_tag_script_js_text, /* <script type="text/javascript" */
    trim_state_tag_script_js_single_quote,
    trim_state_tag_script_js_single_quote_esc,
    trim_state_tag_script_js_double_quote,
    trim_state_tag_script_js_double_quote_esc,
    trim_state_tag_script_js_re_begin,
    trim_state_tag_script_js_re,
    trim_state_tag_script_js_re_esc,
    trim_state_tag_script_js_whitespace,
    trim_state_tag_script_js_comment_begin,
    trim_state_tag_script_js_single_comment,
    trim_state_tag_script_js_single_comment_end,
    trim_state_tag_script_js_multi_comment,
    trim_state_tag_script_js_multi_comment_end,
    trim_state_comment_begin,    /* <!-- */
    trim_state_comment_ie_begin, /* <!--[if */
    trim_state_comment_hack_begin,
    trim_state_comment_end,    /* <!--  --> */
    trim_state_comment_ie_end, /* <!--[if  <![endif]--> */
    trim_state_comment_hack_end,
} ngx_http_trim_state_e;


/* '(' ',' '=' ':' '[' '!' '&' '|' '?' ';' '>' '~' '*' '{' */

static uint32_t trim_js_prefix[] = {
    0x00000000, /* 0000 0000 0000 0000  0000 0000 0000 0000 */

    /* ?>=< ;:98 7654 3210  /.-, +*)( '&%$ #"!  */
    0xec001542, /* 1110 1100 0000 0000  0001 0101 0100 0010 */

    /* _^]\ [ZYX WVUT SRQP  ONML KJIH GFED CBA@ */
    0x08000000, /* 0000 1000 0000 0000  0000 0000 0000 0000 */

    /*  ~}| {zyx wvut srqp  onml kjih gfed cba` */
    0x58000000, /* 0101 1000 0000 0000  0000 0000 0000 0000 */

    0x00000000, /* 0000 0000 0000 0000  0000 0000 0000 0000 */
    0x00000000, /* 0000 0000 0000 0000  0000 0000 0000 0000 */
    0x00000000, /* 0000 0000 0000 0000  0000 0000 0000 0000 */
    0x00000000, /* 0000 0000 0000 0000  0000 0000 0000 0000 */
};


/* ';' '>' '{' '}' ',' ':' */

static uint32_t trim_css_prefix[] = {
    0x00000000, /* 0000 0000 0000 0000  0000 0000 0000 0000 */

    /* ?>=< ;:98 7654 3210  /.-, +*)( '&%$ #"!  */
    0x4c001000, /* 0100 1100 0000 0000  0001 0000 0000 0000 */

    /* _^]\ [ZYX WVUT SRQP  ONML KJIH GFED CBA@ */
    0x00000000, /* 0000 0000 0000 0000  0000 0000 0000 0000 */

    /*  ~}| {zyx wvut srqp  onml kjih gfed cba` */
    0x28000000, /* 0010 1000 0000 0000  0000 0000 0000 0000 */

    0x00000000, /* 0000 0000 0000 0000  0000 0000 0000 0000 */
    0x00000000, /* 0000 0000 0000 0000  0000 0000 0000 0000 */
    0x00000000, /* 0000 0000 0000 0000  0000 0000 0000 0000 */
    0x00000000, /* 0000 0000 0000 0000  0000 0000 0000 0000 */
};


static ngx_int_t ngx_http_trim_parse(ngx_http_request_t *r, ngx_chain_t *in,
                                     ngx_http_trim_ctx_t *ctx);

static ngx_int_t ngx_http_trim_add_variables(ngx_conf_t *cf);
static ngx_int_t ngx_http_trim_bytes_variable(ngx_http_request_t        *r,
                                              ngx_http_variable_value_t *v,
                                              uintptr_t                  data);
static ngx_int_t ngx_http_trim_original_bytes_variable(
    ngx_http_request_t *r, ngx_http_variable_value_t *v, uintptr_t data);

static void     *ngx_http_trim_create_loc_conf(ngx_conf_t *cf);
static char     *ngx_http_trim_merge_loc_conf(ngx_conf_t *cf, void *parent,
                                              void *child);
static ngx_int_t ngx_http_trim_filter_init(ngx_conf_t *cf);


static ngx_command_t ngx_http_trim_commands[] = {

    {ngx_string("trim"),
     NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF
         | NGX_CONF_TAKE1,
     ngx_http_set_complex_value_slot, NGX_HTTP_LOC_CONF_OFFSET,
     offsetof(ngx_http_trim_loc_conf_t, trim), NULL},

    {ngx_string("trim_js"),
     NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF
         | NGX_CONF_TAKE1,
     ngx_http_set_complex_value_slot, NGX_HTTP_LOC_CONF_OFFSET,
     offsetof(ngx_http_trim_loc_conf_t, js), NULL},

    {ngx_string("trim_css"),
     NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF
         | NGX_CONF_TAKE1,
     ngx_http_set_complex_value_slot, NGX_HTTP_LOC_CONF_OFFSET,
     offsetof(ngx_http_trim_loc_conf_t, css), NULL},

    {ngx_string("trim_types"),
     NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF
         | NGX_CONF_1MORE,
     ngx_http_types_slot, NGX_HTTP_LOC_CONF_OFFSET,
     offsetof(ngx_http_trim_loc_conf_t, types_keys),
     &ngx_http_html_default_types[0]},

    ngx_null_command};


static ngx_http_module_t ngx_http_trim_filter_module_ctx = {
    ngx_http_trim_add_variables, /* preconfiguration */
    ngx_http_trim_filter_init,   /* postconfiguration */

    NULL, /* create main configuration */
    NULL, /* init main configuration */

    NULL, /* create server configuration */
    NULL, /* merge server configuration */

    ngx_http_trim_create_loc_conf, /* create location configuration */
    ngx_http_trim_merge_loc_conf   /* merge location configuration */
};


ngx_module_t ngx_http_trim_filter_module = {
    NGX_MODULE_V1,
    &ngx_http_trim_filter_module_ctx, /* module context */
    ngx_http_trim_commands,           /* module directives */
    NGX_HTTP_MODULE,                  /* module type */
    NULL,                             /* init master */
    NULL,                             /* init module */
    NULL,                             /* init process */
    NULL,                             /* init thread */
    NULL,                             /* exit thread */
    NULL,                             /* exit process */
    NULL,                             /* exit master */
    NGX_MODULE_V1_PADDING};


static ngx_http_output_header_filter_pt ngx_http_next_header_filter;
static ngx_http_output_body_filter_pt   ngx_http_next_body_filter;


static ngx_str_t ngx_http_trim_pre            = ngx_string("</pre>");
static ngx_str_t ngx_http_trim_style          = ngx_string("</style>");
static ngx_str_t ngx_http_trim_script         = ngx_string("</script>");
static ngx_str_t ngx_http_trim_style_css      = ngx_string("text/css");
static ngx_str_t ngx_http_trim_script_js      = ngx_string("text/javascript");
static ngx_str_t ngx_http_trim_comment        = ngx_string("-->");
static ngx_str_t ngx_http_trim_textarea       = ngx_string("</textarea>");
static ngx_str_t ngx_http_trim_comment_ie     = ngx_string("[if");
static ngx_str_t ngx_http_trim_comment_ie_end = ngx_string("<![endif]-->");

static ngx_str_t ngx_http_trim_saved_html     = ngx_string("<!--[if");
static ngx_str_t ngx_http_trim_saved_jscss    = ngx_string("/**");
static ngx_str_t ngx_http_trim_saved_css_hack = ngx_string("/*\\*");

static ngx_http_variable_t ngx_http_trim_vars[] = {

    {ngx_string("trim_bytes"), NULL, ngx_http_trim_bytes_variable, 0, 0, 0},

    {ngx_string("trim_original_bytes"), NULL,
     ngx_http_trim_original_bytes_variable, 0, 0, 0},

    {ngx_null_string, NULL, NULL, 0, 0, 0}};


static ngx_int_t
ngx_http_trim_header_filter(ngx_http_request_t *r)
{
    ngx_int_t                 rc;
    ngx_str_t                 flag;
    ngx_http_trim_ctx_t      *ctx;
    ngx_http_trim_loc_conf_t *conf;

    conf = ngx_http_get_module_loc_conf(r, ngx_http_trim_filter_module);

    if (!conf->trim || r->headers_out.status != NGX_HTTP_OK
        || (r->method & NGX_HTTP_HEAD) || r->headers_out.content_length_n == 0
        || (r->headers_out.content_encoding
            && r->headers_out.content_encoding->value.len)
        || ngx_http_test_content_type(r, &conf->types) == NULL)
    {
        return ngx_http_next_header_filter(r);
    }

    rc = ngx_http_arg(r, (u_char *)NGX_HTTP_TRIM_FLAG,
                      sizeof(NGX_HTTP_TRIM_FLAG) - 1, &flag);

    if (rc == NGX_OK && flag.len == sizeof("off") - 1
        && ngx_strncmp(flag.data, "off", sizeof("off") - 1) == 0)
    {
        return ngx_http_next_header_filter(r);
    }

    if (ngx_http_complex_value(r, conf->trim, &flag) != NGX_OK)
    {
        return NGX_ERROR;
    }

    if (!(flag.len == sizeof("on") - 1
          && ngx_strncmp(flag.data, "on", sizeof("on") - 1) == 0))
    {
        return ngx_http_next_header_filter(r);
    }

    ctx = ngx_pcalloc(r->pool, sizeof(ngx_http_trim_ctx_t));
    if (ctx == NULL)
    {
        return NGX_ERROR;
    }

    if (conf->js)
    {
        if (ngx_http_complex_value(r, conf->js, &flag) != NGX_OK)
        {
            return NGX_ERROR;
        }

        if (flag.len == sizeof("on") - 1
            && ngx_strncmp(flag.data, "on", sizeof("on") - 1) == 0)
        {
            ctx->js_enable = 1;
        }
    }

    if (conf->css)
    {
        if (ngx_http_complex_value(r, conf->css, &flag) != NGX_OK)
        {
            return NGX_ERROR;
        }

        if (flag.len == sizeof("on") - 1
            && ngx_strncmp(flag.data, "on", sizeof("on") - 1) == 0)
        {
            ctx->css_enable = 1;
        }
    }

    ctx->prev = ' ';

    ngx_http_set_ctx(r, ctx, ngx_http_trim_filter_module);

    r->filter_need_temporary      = 1;
    r->main_filter_need_in_memory = 1;

    ngx_http_clear_content_length(r);
    ngx_http_clear_accept_ranges(r);

    return ngx_http_next_header_filter(r);
}


static ngx_int_t
ngx_http_trim_body_filter(ngx_http_request_t *r, ngx_chain_t *in)
{
    ngx_int_t            rc;
    ngx_chain_t         *cl, *ln, *out, **ll;
    ngx_http_trim_ctx_t *ctx;

    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
                   "http trim filter");

    ctx = ngx_http_get_module_ctx(r, ngx_http_trim_filter_module);
    if (ctx == NULL)
    {
        return ngx_http_next_body_filter(r, in);
    }

    if (in == NULL)
    {
        return ngx_http_next_body_filter(r, in);
    }

    ctx->in = NULL;
    if (ngx_chain_add_copy(r->pool, &ctx->in, in) != NGX_OK)
    {
        return NGX_ERROR;
    }

    out = NULL;
    ll  = &out;

    for (ln = ctx->in; ln; ln = ln->next)
    {
        ctx->tin += ln->buf->last - ln->buf->pos;

        rc = ngx_http_trim_parse(r, ln, ctx);
        if (rc == NGX_ERROR)
        {
            return rc;
        }

        if (ctx->saved)
        {
            cl = ngx_chain_get_free_buf(r->pool, &ctx->free);
            if (cl == NULL)
            {
                return NGX_ERROR;
            }

            cl->buf->tag    = (ngx_buf_tag_t)&ngx_http_trim_filter_module;
            cl->buf->memory = 1;

            if (ctx->saved > 0)
            {
                cl->buf->pos  = ngx_http_trim_saved_html.data;
                cl->buf->last = cl->buf->pos + ctx->saved;
            }
            else if (ctx->saved == NGX_HTTP_TRIM_SAVE_SLASH)
            {
                cl->buf->pos  = ngx_http_trim_saved_jscss.data;
                cl->buf->last = cl->buf->pos + 1;
            }
            else if (ctx->saved == NGX_HTTP_TRIM_SAVE_SPACE)
            {
                cl->buf->pos  = (u_char *)" ";
                cl->buf->last = cl->buf->pos + 1;
            }
            else if (ctx->saved == NGX_HTTP_TRIM_SAVE_JSCSS)
            {
                cl->buf->pos  = ngx_http_trim_saved_jscss.data;
                cl->buf->last = cl->buf->pos + ngx_http_trim_saved_jscss.len;
            }
            else if (ctx->saved == NGX_HTTP_TRIM_SAVE_HACKCSS)
            {
                cl->buf->pos  = ngx_http_trim_saved_css_hack.data;
                cl->buf->last = cl->buf->pos + ngx_http_trim_saved_css_hack.len;
            }
            else if (ctx->saved == NGX_HTTP_TRIM_SAVE_JAVASCRIPT)
            {
                cl->buf->pos  = ngx_http_trim_script.data;
                cl->buf->last = cl->buf->pos + ngx_http_trim_script.len - 1;
            }

            *ll = cl;
            ll  = &cl->next;

            ctx->tout += cl->buf->last - cl->buf->pos;

            ctx->saved = 0;
        }

        if (ln->buf->in_file
            && (ln->buf->file_last - ln->buf->file_pos)
                   != (off_t)(ln->buf->last - ln->buf->pos))
        {
            ln->buf->in_file = 0;
        }

        if (ngx_buf_size(ln->buf) == 0)
        {
            if (ln->buf->last_buf)
            {
                cl = ngx_chain_get_free_buf(r->pool, &ctx->free);
                if (cl == NULL)
                {
                    return NGX_ERROR;
                }

                ngx_memzero(cl->buf, sizeof(ngx_buf_t));
                cl->buf->tag      = (ngx_buf_tag_t)&ngx_http_trim_filter_module;
                cl->buf->last_buf = 1;

                *ll = cl;
                ll  = &cl->next;
            }
            else
            {
                if (ln->next == NULL)
                {
                    *ll = NULL;
                }
            }
        }
        else
        {
            *ll = ln;
            ll  = &ln->next;
        }
    }

    if (out == NULL)
    {
        return NGX_OK;
    }

    rc = ngx_http_next_body_filter(r, out);

    ngx_chain_update_chains(r->pool, &ctx->free, &ctx->busy, &out,
                            (ngx_buf_tag_t)&ngx_http_trim_filter_module);

    return rc;
}


static ngx_int_t
ngx_http_trim_parse(ngx_http_request_t *r, ngx_chain_t *in,
                    ngx_http_trim_ctx_t *ctx)
{
    u_char    *read, *write, ch, look;
    size_t     size;
    ngx_buf_t *b, *buf;

    b    = in->buf;
    buf  = in->buf;
    size = ngx_buf_size(buf);

    if (size == 0)
    {
        return NGX_OK;
    }

    if (!buf->temporary)
    {
        b = ngx_create_temp_buf(r->pool, size);
        if (b == NULL)
        {
            return NGX_ERROR;
        }

        b->sync          = buf->sync;
        b->flush         = buf->flush;
        b->last_buf      = buf->last_buf;
        b->last_in_chain = buf->last_in_chain;
    }

    for (write = b->pos, read = buf->pos; read < buf->last; read++)
    {
        ch = ngx_tolower(*read);

        switch (ctx->state)
        {
        case trim_state_text:
            switch (ch)
            {
            case '\r': continue;
            case '\n':
                ctx->state = trim_state_text_whitespace;
                if (ctx->prev == '\n')
                {
                    continue;
                }
                else
                {
                    break;
                }
            case '\t':
            case ' ': ctx->state = trim_state_text_whitespace; continue;
            case '<':
                ctx->state         = trim_state_tag;
                ctx->saved_comment = 1;
                continue;
            default: break;
            }
            break;

        case trim_state_tag:
            switch (ch)
            {
            case '\r':
            case '\n':
            case '\t':
            case ' ': ctx->state = trim_state_text_whitespace; break;
            case '!':
                ctx->state  = trim_state_comment_begin;
                ctx->looked = 0; /* --> */
                ctx->saved_comment++;
                continue;
            case 'p':
                ctx->state  = trim_state_tag_pre_begin;
                ctx->looked = 3; /* </pre> */
                break;
            case 't':
                ctx->state  = trim_state_tag_textarea_begin;
                ctx->looked = 3; /* </textarea> */
                break;
            case 's': ctx->state = trim_state_tag_s; break;
            case '<': break;
            case '>': ctx->state = trim_state_text; break;
            default:
                if ((ch >= 'a' && ch <= 'z') || ch == '/')
                {
                    ctx->state = trim_state_tag_text;
                }
                else
                {
                    ctx->state = trim_state_text;
                }
                break;
            }

            if ((size_t)(read - buf->pos) >= ctx->saved_comment)
            {
                write = ngx_cpymem(write, ngx_http_trim_saved_html.data,
                                   ctx->saved_comment);
            }
            else
            {
                ctx->saved = ctx->saved_comment;
            }

            if (ctx->state == trim_state_tag
                || ctx->state == trim_state_text_whitespace)
            {
                ctx->prev = '<';
                continue;
            }

            break;

        case trim_state_tag_text:
            switch (ch)
            {
            case '\r':
            case '\n':
            case '\t':
            case ' ': ctx->state = trim_state_tag_whitespace; continue;
            case '>': ctx->state = trim_state_text; break;
            default: break;
            }
            break;

        case trim_state_tag_attribute:
            switch (ch)
            {
            case '\r':
            case '\n':
            case '\t':
            case ' ':
                if (ctx->prev != '=')
                {
                    ctx->state = trim_state_tag_whitespace;
                }
                continue;
            case '\'': ctx->state = trim_state_tag_single_quote; break;
            case '"': ctx->state = trim_state_tag_double_quote; break;
            case '>':
                if (ctx->tag == NGX_HTTP_TRIM_TAG_PRE)
                {
                    ctx->state = trim_state_tag_pre;
                }
                else if (ctx->tag == NGX_HTTP_TRIM_TAG_TEXTAREA)
                {
                    ctx->state = trim_state_tag_textarea_end;
                }
                else if (ctx->tag == NGX_HTTP_TRIM_TAG_SCRIPT)
                {
                    if (ctx->js_enable
                        && ctx->looked == ngx_http_trim_script_js.len)
                    {
                        ctx->state = trim_state_tag_script_js_text;
                    }
                    else
                    {
                        ctx->state = trim_state_tag_script_end;
                    }
                }
                else if (ctx->tag == NGX_HTTP_TRIM_TAG_STYLE)
                {
                    if (ctx->css_enable
                        && ctx->looked == ngx_http_trim_style_css.len)
                    {
                        ctx->state = trim_state_tag_style_css_text;
                    }
                    else
                    {
                        ctx->state = trim_state_tag_style_end;
                    }
                }
                else
                {
                    ctx->state = trim_state_text;
                }

                ctx->tag    = 0;
                ctx->looked = 0;
                break;
            default: break;
            }
            break;

        case trim_state_tag_s:
            switch (ch)
            {
            case '\r':
            case '\n':
            case '\t':
            case ' ': ctx->state = trim_state_tag_whitespace; continue;
            case 't':
                ctx->state  = trim_state_tag_style_begin;
                ctx->looked = 4; /* </style> */
                break;
            case 'c':
                ctx->state  = trim_state_tag_script_begin;
                ctx->looked = 4; /* </script> */
                break;
            case '>': ctx->state = trim_state_text; break;
            default: ctx->state = trim_state_tag_text; break;
            }
            break;

        case trim_state_comment_begin:
            look = ngx_http_trim_comment.data[ctx->looked++];
            if (ch == look)
            {
                if (ctx->looked == ngx_http_trim_comment.len - 1)
                { /* --> */
                    ctx->state  = trim_state_comment_hack_begin;
                    ctx->looked = 0;
                }

                ctx->saved_comment++;
                continue;
            }

            switch (ch)
            {
            case '\r':
            case '\n':
            case '\t':
            case ' ': ctx->state = trim_state_tag_whitespace; continue;
            case '>': ctx->state = trim_state_text; break;
            default: ctx->state = trim_state_tag_text; break;
            }

            if ((size_t)(read - buf->pos) >= ctx->saved_comment)
            {
                write = ngx_cpymem(write, ngx_http_trim_saved_html.data,
                                   ctx->saved_comment);
            }
            else
            {
                ctx->saved = ctx->saved_comment;
            }

            break;

        case trim_state_comment_hack_begin:
            switch (ch)
            {
            case '#':
                ctx->state  = trim_state_comment_hack_end;
                ctx->looked = 0;
                break;
            case 'e':
                ctx->state  = trim_state_comment_hack_end;
                ctx->looked = 0;
                break;
            case '[':
                ctx->state  = trim_state_comment_ie_begin;
                ctx->looked = 1;
                ctx->saved_comment++;
                continue;
            case '-':
                ctx->state  = trim_state_comment_end;
                ctx->looked = 1;
                continue;
            default:
                ctx->state  = trim_state_comment_end;
                ctx->looked = 0;
                continue;
            }

            if ((size_t)(read - buf->pos) >= ctx->saved_comment)
            {
                write = ngx_cpymem(write, ngx_http_trim_saved_html.data,
                                   ctx->saved_comment);
            }
            else
            {
                ctx->saved = ctx->saved_comment;
            }

            break;

        case trim_state_comment_ie_begin:
            look = ngx_http_trim_comment_ie.data[ctx->looked++];
            if (ch == look)
            {
                if (ctx->looked == ngx_http_trim_comment_ie.len)
                { /* [if */
                    ctx->state  = trim_state_comment_ie_end;
                    ctx->looked = 0;

                    if ((size_t)(read - buf->pos) >= ctx->saved_comment)
                    {
                        write = ngx_cpymem(write, ngx_http_trim_saved_html.data,
                                           ctx->saved_comment);
                    }
                    else
                    {
                        ctx->saved = ctx->saved_comment;
                    }

                    break;
                }

                ctx->saved_comment++;
                continue;
            }

            switch (ch)
            {
            case '-':
                ctx->state  = trim_state_comment_end;
                ctx->looked = 1;
                break;
            default:
                ctx->state  = trim_state_comment_end;
                ctx->looked = 0;
                break;
            }

            continue;

        case trim_state_tag_pre_begin:
            look = ngx_http_trim_pre.data[ctx->looked++]; /* <pre> */
            if (ch == look)
            {
                if (ctx->looked == ngx_http_trim_pre.len)
                {
                    ctx->state  = trim_state_tag_pre;
                    ctx->count  = 1;
                    ctx->looked = 0;
                }
                break;
            }

            switch (ch)
            {
            case '\r':
            case '\n':
            case '\t':
            case ' ':
                if (ctx->looked == ngx_http_trim_pre.len)
                {
                    ctx->tag   = NGX_HTTP_TRIM_TAG_PRE;
                    ctx->count = 1;
                }

                ctx->state  = trim_state_tag_whitespace;
                ctx->looked = 0;
                continue;
            case '>': ctx->state = trim_state_text; break;
            default: ctx->state = trim_state_tag_text; break;
            }
            break;

        case trim_state_tag_textarea_begin:
            look = ngx_http_trim_textarea.data[ctx->looked++]; /* <textarea> */
            if (ch == look)
            {
                if (ctx->looked == ngx_http_trim_textarea.len)
                {
                    ctx->state  = trim_state_tag_textarea_end;
                    ctx->looked = 0;
                }
                break;
            }

            switch (ch)
            {
            case '\r':
            case '\n':
            case '\t':
            case ' ':
                if (ctx->looked == ngx_http_trim_textarea.len)
                {
                    ctx->tag = NGX_HTTP_TRIM_TAG_TEXTAREA;
                }

                ctx->state  = trim_state_tag_whitespace;
                ctx->looked = 0;
                continue;
            case '>': ctx->state = trim_state_text; break;
            default: ctx->state = trim_state_tag_text; break;
            }
            break;

        case trim_state_tag_script_begin:
            look = ngx_http_trim_script.data[ctx->looked++]; /* <script> */
            if (ch == look)
            {
                if (ctx->looked == ngx_http_trim_script.len)
                {
                    if (ctx->js_enable)
                    {
                        ctx->state = trim_state_tag_script_js_text;
                    }
                    else
                    {
                        ctx->state = trim_state_tag_script_end;
                    }

                    ctx->looked = 0;
                }
                break;
            }

            switch (ch)
            {
            case '\r':
            case '\n':
            case '\t':
            case ' ':
                if (ctx->looked == ngx_http_trim_script.len)
                {
                    ctx->tag = NGX_HTTP_TRIM_TAG_SCRIPT;
                }

                ctx->state  = trim_state_tag_whitespace;
                ctx->looked = 0;
                continue;
            case '>': ctx->state = trim_state_text; break;
            default: ctx->state = trim_state_tag_text; break;
            }
            break;

        case trim_state_tag_script_js_text:
            switch (ch)
            {
            case '\r': continue;
            case '\n':
            case '\t':
            case ' ':
                ctx->state = trim_state_tag_script_js_whitespace;
                if (trim_js_prefix[ctx->prev >> 5] & (1 << (ctx->prev & 0x1f))
                    || ctx->prev == ch)
                {
                    continue;
                }
                else
                {
                    break;
                }
            case '\'':
                ctx->state = trim_state_tag_script_js_single_quote;
                break;
            case '"': ctx->state = trim_state_tag_script_js_double_quote; break;
            case '<':
                ctx->state  = trim_state_tag_script_js_end;
                ctx->looked = 1;
                break;
            case '/':
                if (trim_js_prefix[ctx->prev >> 5] & (1 << (ctx->prev & 0x1f))
                    || ctx->prev == '+' || ctx->prev == '-')
                {
                    ctx->state = trim_state_tag_script_js_re_begin;
                }
                else
                {
                    ctx->state = trim_state_tag_script_js_comment_begin;
                }
                continue;
            default: break;
            }
            break;

        case trim_state_tag_script_js_single_quote:
            switch (ch)
            {
            case '\\':
                ctx->state = trim_state_tag_script_js_single_quote_esc;
                break;
            case '\'': ctx->state = trim_state_tag_script_js_text; break;
            default: break;
            }
            break;

        case trim_state_tag_script_js_double_quote:
            switch (ch)
            {
            case '\\':
                ctx->state = trim_state_tag_script_js_double_quote_esc;
                break;
            case '"': ctx->state = trim_state_tag_script_js_text; break;
            default: break;
            }
            break;

        case trim_state_tag_script_js_single_quote_esc:
            ctx->state = trim_state_tag_script_js_single_quote;
            break;

        case trim_state_tag_script_js_double_quote_esc:
            ctx->state = trim_state_tag_script_js_double_quote;
            break;

        case trim_state_tag_script_js_re_begin:
            switch (ch)
            {
            case '/':
                ctx->state = trim_state_tag_script_js_single_comment;
                continue;
            case '*':
                ctx->state = trim_state_tag_script_js_multi_comment;
                continue;
            case '\\':
                ctx->state = trim_state_tag_script_js_re_esc;
                if (read > buf->pos)
                {
                    *write++ = '/';
                }
                else
                {
                    ctx->saved = NGX_HTTP_TRIM_SAVE_SLASH;
                }
                break;
            default:
                ctx->state = trim_state_tag_script_js_re;
                if (read > buf->pos)
                {
                    *write++ = '/';
                }
                else
                {
                    ctx->saved = NGX_HTTP_TRIM_SAVE_SLASH;
                }
                break;
            }
            break;

        case trim_state_tag_script_js_re:
            switch (ch)
            {
            case '/': ctx->state = trim_state_tag_script_js_text; break;
            case '\\': ctx->state = trim_state_tag_script_js_re_esc; break;
            default: break;
            }
            break;

        case trim_state_tag_script_js_re_esc:
            ctx->state = trim_state_tag_script_js_re;
            break;

        case trim_state_tag_script_js_comment_begin:
            switch (ch)
            {
            case '/':
                ctx->state = trim_state_tag_script_js_single_comment;
                continue;
            case '*':
                ctx->state = trim_state_tag_script_js_multi_comment;
                continue;
            default:
                ctx->state = trim_state_tag_script_js_text;
                if (read > buf->pos)
                {
                    *write++ = '/';
                }
                else
                {
                    ctx->saved = NGX_HTTP_TRIM_SAVE_SLASH;
                }
                break;
            }
            break;

        case trim_state_tag_script_js_single_comment:
            switch (ch)
            {
            case '<':
                ctx->looked = 1;
                ctx->state  = trim_state_tag_script_js_single_comment_end;
                continue;
            case '\n':
                ctx->state = trim_state_tag_script_js_text;
                if (trim_js_prefix[ctx->prev >> 5] & (1 << (ctx->prev & 0x1f))
                    || ctx->prev == ch)
                {
                    continue;
                }
                else
                {
                    break;
                }
            default: continue;
            }
            break;

        case trim_state_tag_script_js_single_comment_end:
            look = ngx_http_trim_script.data[ctx->looked++];
            if (ch == look)
            {
                if (ctx->looked == ngx_http_trim_script.len)
                {
                    ctx->state  = trim_state_text;
                    ctx->looked = 0;

                    if ((size_t)(read - buf->pos)
                        >= ngx_http_trim_script.len - 1)
                    {
                        write = ngx_cpymem(write, ngx_http_trim_script.data,
                                           ngx_http_trim_script.len - 1);
                    }
                    else
                    {
                        ctx->saved = NGX_HTTP_TRIM_SAVE_JAVASCRIPT;
                    }

                    break;
                }

                continue;
            }

            switch (ch)
            {
            case '\r':
            case '\n':
            case '\t':
            case ' ':
                if (ctx->looked == ngx_http_trim_script.len)
                {
                    ctx->state = trim_state_tag_whitespace;

                    if ((size_t)(read - buf->pos)
                        >= ngx_http_trim_script.len - 1)
                    {
                        write = ngx_cpymem(write, ngx_http_trim_script.data,
                                           ngx_http_trim_script.len - 1);
                    }
                    else
                    {
                        ctx->saved = NGX_HTTP_TRIM_SAVE_JAVASCRIPT;
                    }

                    ctx->prev   = 't';
                    ctx->looked = 0;
                    continue;
                }
                ctx->looked = 0;
                break;
            case '<': ctx->looked = 1; break;
            default:
                ctx->state  = trim_state_tag_script_js_single_comment;
                ctx->looked = 0;
                break;
            }

            continue;

        case trim_state_tag_script_js_multi_comment:
            switch (ch)
            {
            case '*':
                ctx->state = trim_state_tag_script_js_multi_comment_end;
                break;
            default: break;
            }
            continue;

        case trim_state_tag_script_js_multi_comment_end:
            switch (ch)
            {
            case '/': ctx->state = trim_state_tag_script_js_text; break;
            case '*': break;
            default: ctx->state = trim_state_tag_script_js_multi_comment; break;
            }
            continue;

        case trim_state_tag_script_end:
            look = ngx_http_trim_script.data[ctx->looked++];
            if (ch == look)
            {
                if (ctx->looked == ngx_http_trim_script.len)
                {
                    ctx->state = trim_state_text;
                }
                break;
            }

            switch (ch)
            {
            case '\r':
            case '\n':
            case '\t':
            case ' ':
                if (ctx->looked == ngx_http_trim_script.len)
                {
                    ctx->state  = trim_state_tag_whitespace;
                    ctx->looked = 0;
                    continue;
                }

                ctx->looked = 0;
                break;
            case '<': ctx->looked = 1; break;
            default: ctx->looked = 0; break;
            }
            break;

        case trim_state_tag_script_js_end:
            look = ngx_http_trim_script.data[ctx->looked++];
            if (ch == look)
            {
                if (ctx->looked == ngx_http_trim_script.len)
                {
                    ctx->state = trim_state_text;
                }
                break;
            }

            switch (ch)
            {
            case '\r':
            case '\n':
            case '\t':
            case ' ':
                if (ctx->looked == ngx_http_trim_script.len)
                {
                    ctx->state  = trim_state_tag_whitespace;
                    ctx->looked = 0;
                    continue;
                }

                ctx->looked = 0;
                break;
            case '<': ctx->looked = 1; break;
            default:
                ctx->state  = trim_state_tag_script_js_text;
                ctx->looked = 0;
                break;
            }
            break;

        case trim_state_tag_script_js_whitespace:
            switch (ch)
            {
            case '\n':
                if (trim_js_prefix[ctx->prev >> 5] & (1 << (ctx->prev & 0x1f))
                    || ctx->prev == ch)
                {
                    continue;
                }
                else
                {
                    break;
                }
            case '\r':
            case '\t':
            case ' ': continue;
            case '\'':
                ctx->state = trim_state_tag_script_js_single_quote;
                break;
            case '"': ctx->state = trim_state_tag_script_js_double_quote; break;
            case '<':
                ctx->state  = trim_state_tag_script_js_end;
                ctx->looked = 1;
                break;
            case '/':
                if (trim_js_prefix[ctx->prev >> 5] & (1 << (ctx->prev & 0x1f))
                    || ctx->prev == '+' || ctx->prev == '-')
                {
                    ctx->state = trim_state_tag_script_js_re_begin;
                }
                else
                {
                    ctx->state = trim_state_tag_script_js_comment_begin;
                }
                continue;
            default: ctx->state = trim_state_tag_script_js_text; break;
            }
            break;

        case trim_state_tag_style_begin:
            look = ngx_http_trim_style.data[ctx->looked++]; /* <style> */
            if (ch == look)
            {
                if (ctx->looked == ngx_http_trim_style.len)
                {
                    if (ctx->css_enable)
                    {
                        ctx->state = trim_state_tag_style_css_text;
                    }
                    else
                    {
                        ctx->state = trim_state_tag_style_end;
                    }

                    ctx->looked = 0;
                }
                break;
            }

            switch (ch)
            {
            case '\r':
            case '\n':
            case '\t':
            case ' ':
                if (ctx->looked == ngx_http_trim_style.len)
                {
                    ctx->tag = NGX_HTTP_TRIM_TAG_STYLE;
                }

                ctx->state  = trim_state_tag_whitespace;
                ctx->looked = 0;
                continue;
            case '>': ctx->state = trim_state_text; break;
            default: ctx->state = trim_state_tag_text; break;
            }
            break;

        case trim_state_tag_style_css_text:
            switch (ch)
            {
            case '\r': continue;
            case '\n':
            case '\t':
            case ' ':
                if (!(trim_css_prefix[ctx->prev >> 5]
                      & (1 << (ctx->prev & 0x1f))))
                {
                    ctx->state = trim_state_tag_style_css_whitespace;
                }
                continue;
            case '\'':
                ctx->state = trim_state_tag_style_css_single_quote;
                break;
            case '"': ctx->state = trim_state_tag_style_css_double_quote; break;
            case '<':
                ctx->state  = trim_state_tag_style_css_end;
                ctx->looked = 1;
                break;
            case '/':
                ctx->state = trim_state_tag_style_css_comment_begin;
                continue;
            default: break;
            }
            break;

        case trim_state_tag_style_css_single_quote:
            switch (ch)
            {
            case '\\':
                ctx->state = trim_state_tag_style_css_single_quote_esc;
                break;
            case '\'': ctx->state = trim_state_tag_style_css_text; break;
            default: break;
            }
            break;

        case trim_state_tag_style_css_double_quote:
            switch (ch)
            {
            case '\\':
                ctx->state = trim_state_tag_style_css_double_quote_esc;
                break;
            case '"': ctx->state = trim_state_tag_style_css_text; break;
            default: break;
            }
            break;

        case trim_state_tag_style_css_single_quote_esc:
            ctx->state = trim_state_tag_style_css_single_quote;
            break;

        case trim_state_tag_style_css_double_quote_esc:
            ctx->state = trim_state_tag_style_css_double_quote;
            break;

        case trim_state_tag_style_css_comment_begin:
            switch (ch)
            {
            case '*':
                ctx->state = trim_state_tag_style_css_comment_begin_empty;
                continue;
            case '/':
                ctx->state = trim_state_tag_style_css_comment_begin;
                break;
            default: ctx->state = trim_state_tag_style_css_text; break;
            }

            if (read > buf->pos)
            {
                *write++ = '/';
            }
            else
            {
                ctx->saved = NGX_HTTP_TRIM_SAVE_SLASH;
            }

            if (ch == '/')
            {
                continue;
            }
            break;

        case trim_state_tag_style_css_comment_begin_empty:
            switch (ch)
            {
            case '*':
                ctx->state = trim_state_tag_style_css_comment_empty;
                break;
            case '\\':
                ctx->state = trim_state_tag_style_css_comment_begin_hack;
                break;
            default: ctx->state = trim_state_tag_style_css_comment; break;
            }
            continue;

        case trim_state_tag_style_css_comment:
            switch (ch)
            {
            case '*': ctx->state = trim_state_tag_style_css_comment_end; break;
            case '\\':
                ctx->state = trim_state_tag_style_css_comment_begin_hack;
                break;
            default: break;
            }
            continue;

        case trim_state_tag_style_css_comment_empty:
            switch (ch)
            {
            case '/':
                ctx->state = trim_state_tag_style_css_text;

                if ((size_t)(read - buf->pos) >= ngx_http_trim_saved_jscss.len)
                {
                    write = ngx_cpymem(write, ngx_http_trim_saved_jscss.data,
                                       ngx_http_trim_saved_jscss.len);
                }
                else
                {
                    ctx->saved = NGX_HTTP_TRIM_SAVE_JSCSS;
                }
                break;
            case '*':
                ctx->state = trim_state_tag_style_css_comment_end;
                continue;
            case '\\':
                ctx->state = trim_state_tag_style_css_comment_begin_hack;
                continue;
            default: ctx->state = trim_state_tag_style_css_comment; continue;
            }
            break;

        case trim_state_tag_style_css_comment_begin_hack:
            switch (ch)
            {
            case '*': ctx->state = trim_state_tag_style_css_comment_hack; break;
            default: ctx->state = trim_state_tag_style_css_comment; break;
            }
            continue;

        case trim_state_tag_style_css_comment_hack:
            switch (ch)
            {
            case '/':
                ctx->state = trim_state_tag_style_css_comment_hack_text;

                if ((size_t)(read - buf->pos)
                    >= ngx_http_trim_saved_css_hack.len)
                {
                    write = ngx_cpymem(write, ngx_http_trim_saved_css_hack.data,
                                       ngx_http_trim_saved_css_hack.len);
                }
                else
                {
                    ctx->saved = NGX_HTTP_TRIM_SAVE_HACKCSS;
                }
                break;
            case '*':
                ctx->state = trim_state_tag_style_css_comment_end;
                continue;
            case '\\':
                ctx->state = trim_state_tag_style_css_comment_begin_hack;
                continue;
            default: ctx->state = trim_state_tag_style_css_comment; continue;
            }
            break;

        case trim_state_tag_style_css_comment_hack_text:
            switch (ch)
            {
            case '/':
                ctx->state = trim_state_tag_style_css_comment_hack_text_begin;
                break;
            default: break;
            }
            break;

        case trim_state_tag_style_css_comment_hack_text_begin:
            switch (ch)
            {
            case '*':
                ctx->state = trim_state_tag_style_css_comment_hack_text_end;
                break;
            case '/': break;
            default:
                ctx->state = trim_state_tag_style_css_comment_hack_text;
                break;
            }
            break;

        case trim_state_tag_style_css_comment_hack_text_end:
            switch (ch)
            {
            case '*':
                ctx->state = trim_state_tag_style_css_comment_hack_text_last;
                break;
            default: continue;
            }
            break;

        case trim_state_tag_style_css_comment_hack_text_last:
            switch (ch)
            {
            case '*':
                ctx->state = trim_state_tag_style_css_comment_hack_text_last;
                break;
            case '/': ctx->state = trim_state_tag_style_css_text; break;
            default:
                ctx->state = trim_state_tag_style_css_comment_hack_text_end;
                break;
            }
            break;

        case trim_state_tag_style_css_comment_end:
            switch (ch)
            {
            case '/': ctx->state = trim_state_tag_style_css_text; break;
            case '*': break;
            case '\\':
                ctx->state = trim_state_tag_style_css_comment_begin_hack;
                break;
            default: ctx->state = trim_state_tag_style_css_comment; break;
            }
            continue;

        case trim_state_tag_style_css_whitespace:
            switch (ch)
            {
            case '\r':
            case '\n':
            case '\t':
            case ' ': continue;
            case '\'':
                ctx->state = trim_state_tag_style_css_single_quote;
                break;
            case '"': ctx->state = trim_state_tag_style_css_double_quote; break;
            case '<':
                ctx->state  = trim_state_tag_style_css_end;
                ctx->looked = 1;
                break;
            case '/':
                ctx->state = trim_state_tag_style_css_comment_begin;
                break;
            default: ctx->state = trim_state_tag_style_css_text; break;
            }

            if (!(trim_css_prefix[ch >> 5] & (1 << (ch & 0x1f))))
            {
                if (read > buf->pos)
                {
                    *write++ = ' ';
                }
                else
                {
                    ctx->saved = NGX_HTTP_TRIM_SAVE_SPACE;
                }
            }

            if (ch == '/')
            {
                continue;
            }

            break;

        case trim_state_tag_style_end:
            look = ngx_http_trim_style.data[ctx->looked++];
            if (ch == look)
            {
                if (ctx->looked == ngx_http_trim_style.len)
                {
                    ctx->state = trim_state_text;
                }
                break;
            }

            switch (ch)
            {
            case '\r':
            case '\n':
            case '\t':
            case ' ':
                if (ctx->looked == ngx_http_trim_style.len)
                {
                    ctx->state  = trim_state_tag_whitespace;
                    ctx->looked = 0;
                    continue;
                }

                ctx->looked = 0;
                break;
            case '<': ctx->looked = 1; break;
            default: ctx->looked = 0; break;
            }
            break;

        case trim_state_tag_style_css_end:
            look = ngx_http_trim_style.data[ctx->looked++];
            if (ch == look)
            {
                if (ctx->looked == ngx_http_trim_style.len)
                {
                    ctx->state = trim_state_text;
                }
                break;
            }

            switch (ch)
            {
            case '\r':
            case '\n':
            case '\t':
            case ' ':
                if (ctx->looked == ngx_http_trim_style.len)
                {
                    ctx->state  = trim_state_tag_whitespace;
                    ctx->looked = 0;
                    continue;
                }

                ctx->looked = 0;
                break;
            case '<': ctx->looked = 1; break;
            default:
                ctx->state  = trim_state_tag_style_css_text;
                ctx->looked = 0;
                break;
            }
            break;

        case trim_state_comment_end:
            look = ngx_http_trim_comment.data[ctx->looked++]; /* --> */
            if (ch == look)
            {
                if (ctx->looked == ngx_http_trim_comment.len)
                {
                    ctx->state = trim_state_text;
                }
                continue;
            }

            switch (ch)
            {
            case '-': ctx->looked--; break;
            default: ctx->looked = 0; break;
            }
            continue;

        case trim_state_comment_ie_end: /*  <![endif]-->  */
            look = ngx_http_trim_comment_ie_end.data[ctx->looked++];
            if (ch == look)
            {
                if (ctx->looked == ngx_http_trim_comment_ie_end.len)
                {
                    ctx->state = trim_state_text;
                }
                break;
            }

            switch (ch)
            {
            case '<': ctx->looked = 1; break;
            default: ctx->looked = 0; break;
            }
            break;

        case trim_state_comment_hack_end:
            look = ngx_http_trim_comment.data[ctx->looked++]; /* --> */
            if (ch == look)
            {
                if (ctx->looked == ngx_http_trim_comment.len)
                {
                    ctx->state = trim_state_text;
                }
                break;
            }

            switch (ch)
            {
            case '-': ctx->looked--; break;
            default: ctx->looked = 0; break;
            }

            break;

        case trim_state_tag_pre:
            switch (ch)
            {
            case '<': ctx->state = trim_state_tag_pre_angle; break;
            default: break;
            }
            break;

        case trim_state_tag_pre_angle:
            switch (ch)
            {
            case '/':
                ctx->state  = trim_state_tag_pre_end;
                ctx->looked = 2;
                break;
            case 'p':
                ctx->state  = trim_state_tag_pre_nest;
                ctx->looked = 3;
                break;
            case '<': break;
            default: ctx->state = trim_state_tag_pre; break;
            }
            break;

        case trim_state_tag_pre_nest:
            look = ngx_http_trim_pre.data[ctx->looked++];
            if (ch == look)
            {
                if (ctx->looked == ngx_http_trim_pre.len)
                {
                    ctx->count++;
                    ctx->state = trim_state_tag_pre;
                }
                break;
            }

            switch (ch)
            {
            case '\r':
            case '\n':
            case '\t':
            case ' ':
                if (ctx->looked == ngx_http_trim_pre.len)
                {
                    ctx->count++;
                    ctx->tag   = NGX_HTTP_TRIM_TAG_PRE;
                    ctx->state = trim_state_tag_whitespace;
                    continue;
                }
                else
                {
                    ctx->state = trim_state_tag_pre;
                }

                break;
            case '<': ctx->state = trim_state_tag_pre_angle; break;
            default: ctx->state = trim_state_tag_pre; break;
            }
            break;

        case trim_state_tag_pre_end:
            look = ngx_http_trim_pre.data[ctx->looked++];
            if (ch == look)
            {
                if (ctx->looked == ngx_http_trim_pre.len)
                {
                    if (--ctx->count > 0)
                    {
                        ctx->state = trim_state_tag_pre;
                    }
                    else
                    {
                        ctx->state = trim_state_text;
                    }
                }
                break;
            }

            switch (ch)
            {
            case '\r':
            case '\n':
            case '\t':
            case ' ':
                if (ctx->looked == ngx_http_trim_pre.len)
                {
                    if (--ctx->count > 0)
                    {
                        ctx->tag = NGX_HTTP_TRIM_TAG_PRE;
                    }

                    ctx->state  = trim_state_tag_whitespace;
                    ctx->looked = 0;
                    continue;
                }

                ctx->looked = 0;
                break;
            case '<': ctx->state = trim_state_tag_pre_angle; break;
            default: ctx->state = trim_state_tag_pre; break;
            }
            break;

        case trim_state_tag_textarea_end:
            look = ngx_http_trim_textarea.data[ctx->looked++];
            if (ch == look)
            {
                if (ctx->looked == ngx_http_trim_textarea.len)
                {
                    ctx->state = trim_state_text;
                }
                break;
            }

            switch (ch)
            {
            case '\r':
            case '\n':
            case '\t':
            case ' ':
                if (ctx->looked == ngx_http_trim_textarea.len)
                {
                    ctx->state  = trim_state_tag_whitespace;
                    ctx->looked = 0;
                    continue;
                }

                ctx->looked = 0;
                break;
            case '<': ctx->looked = 1; break;
            default: ctx->looked = 0; break;
            }
            break;

        case trim_state_text_whitespace:
            switch (ch)
            {
            case '\r':
            case '\t':
            case ' ': continue;
            case '\n':
                if (ctx->prev == '\n')
                {
                    continue;
                }
                else
                {
                    break;
                }
            case '<':
                ctx->state         = trim_state_tag;
                ctx->saved_comment = 1;
                break;
            default: ctx->state = trim_state_text; break;
            }

            if (ch != '\n' && ctx->prev != '\n')
            {
                if (read > buf->pos)
                {
                    *write++ = ' ';
                }
                else
                {
                    ctx->saved = NGX_HTTP_TRIM_SAVE_SPACE;
                }
            }

            if (ch == '<')
            {
                continue;
            }

            break;

        case trim_state_tag_whitespace:
            switch (ch)
            {
            case '\r':
            case '\n':
            case '\t':
            case ' ': continue;
            case '\'': ctx->state = trim_state_tag_single_quote; break;
            case '"': ctx->state = trim_state_tag_double_quote; break;
            case '>':
                if (ctx->tag == NGX_HTTP_TRIM_TAG_PRE)
                {
                    ctx->state = trim_state_tag_pre;
                }
                else if (ctx->tag == NGX_HTTP_TRIM_TAG_TEXTAREA)
                {
                    ctx->state = trim_state_tag_textarea_end;
                }
                else if (ctx->tag == NGX_HTTP_TRIM_TAG_SCRIPT)
                {
                    if (ctx->js_enable
                        && ctx->looked == ngx_http_trim_script_js.len)
                    {
                        ctx->state = trim_state_tag_script_js_text;
                    }
                    else
                    {
                        ctx->state = trim_state_tag_script_end;
                    }
                }
                else if (ctx->tag == NGX_HTTP_TRIM_TAG_STYLE)
                {
                    if (ctx->css_enable
                        && ctx->looked == ngx_http_trim_style_css.len)
                    {
                        ctx->state = trim_state_tag_style_css_text;
                    }
                    else
                    {
                        ctx->state = trim_state_tag_style_end;
                    }
                }
                else
                {
                    ctx->state = trim_state_text;
                }

                ctx->tag    = 0;
                ctx->looked = 0;
                break;
            default: ctx->state = trim_state_tag_attribute; break;
            }

            if (ch != '>' && ch != '=')
            {
                if (read > buf->pos)
                {
                    *write++ = ' ';
                }
                else
                {
                    ctx->saved = NGX_HTTP_TRIM_SAVE_SPACE;
                }
            }

            break;

        case trim_state_tag_single_quote:
            switch (ch)
            {
            case '\'': ctx->state = trim_state_tag_attribute; break;
            default: break;
            }

            if (ctx->js_enable && ctx->tag == NGX_HTTP_TRIM_TAG_SCRIPT)
            {
                if (ctx->looked != ngx_http_trim_script_js.len)
                {
                    look = ngx_http_trim_script_js.data[ctx->looked++];
                    if (ch != look)
                    {
                        ctx->looked = 0;
                    }
                }
            }

            if (ctx->css_enable && ctx->tag == NGX_HTTP_TRIM_TAG_STYLE)
            {
                if (ctx->looked != ngx_http_trim_style_css.len)
                {
                    look = ngx_http_trim_style_css.data[ctx->looked++];
                    if (ch != look)
                    {
                        ctx->looked = 0;
                    }
                }
            }

            break;

        case trim_state_tag_double_quote:
            switch (ch)
            {
            case '"': ctx->state = trim_state_tag_attribute; break;
            default: break;
            }

            if (ctx->js_enable && ctx->tag == NGX_HTTP_TRIM_TAG_SCRIPT)
            {
                if (ctx->looked != ngx_http_trim_script_js.len)
                {
                    look = ngx_http_trim_script_js.data[ctx->looked++];
                    if (ch != look)
                    {
                        ctx->looked = 0;
                    }
                }
            }

            if (ctx->css_enable && ctx->tag == NGX_HTTP_TRIM_TAG_STYLE)
            {
                if (ctx->looked != ngx_http_trim_style_css.len)
                {
                    look = ngx_http_trim_style_css.data[ctx->looked++];
                    if (ch != look)
                    {
                        ctx->looked = 0;
                    }
                }
            }

            break;

        default: break;
        }

        *write++  = *read;
        ctx->prev = *read;
    }

    if (!buf->temporary)
    {
        in->buf  = b;
        buf->pos = buf->last;
    }

    b->last = write;
    ctx->tout += b->last - b->pos;

    return NGX_OK;
}

static ngx_int_t
ngx_http_trim_add_variables(ngx_conf_t *cf)
{
    ngx_http_variable_t *var, *v;

    for (v = ngx_http_trim_vars; v->name.len; v++)
    {
        var = ngx_http_add_variable(cf, &v->name, v->flags);
        if (var == NULL)
        {
            return NGX_ERROR;
        }

        var->get_handler = v->get_handler;
        var->data        = v->data;
    }

    return NGX_OK;
}


static ngx_int_t
ngx_http_trim_bytes_variable(ngx_http_request_t        *r,
                             ngx_http_variable_value_t *v, uintptr_t data)
{
    ngx_http_trim_ctx_t *ctx;

    ctx = ngx_http_get_module_ctx(r, ngx_http_trim_filter_module);

    if (ctx == NULL)
    {
        v->not_found = 1;
        return NGX_OK;
    }

    v->data = ngx_pnalloc(r->pool, NGX_OFF_T_LEN);
    if (v->data == NULL)
    {
        return NGX_ERROR;
    }

    v->len   = ngx_sprintf(v->data, "%O", ctx->tin - ctx->tout) - v->data;
    v->valid = 1;
    v->no_cacheable = 0;
    v->not_found    = 0;

    return NGX_OK;
}


static ngx_int_t
ngx_http_trim_original_bytes_variable(ngx_http_request_t        *r,
                                      ngx_http_variable_value_t *v,
                                      uintptr_t                  data)
{
    ngx_http_trim_ctx_t *ctx;

    ctx = ngx_http_get_module_ctx(r, ngx_http_trim_filter_module);

    if (ctx == NULL)
    {
        v->not_found = 1;
        return NGX_OK;
    }

    v->data = ngx_pnalloc(r->pool, NGX_OFF_T_LEN);
    if (v->data == NULL)
    {
        return NGX_ERROR;
    }

    v->len          = ngx_sprintf(v->data, "%O", ctx->tin) - v->data;
    v->valid        = 1;
    v->no_cacheable = 0;
    v->not_found    = 0;

    return NGX_OK;
}


static void *
ngx_http_trim_create_loc_conf(ngx_conf_t *cf)
{
    ngx_http_trim_loc_conf_t *conf;

    conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_trim_loc_conf_t));
    if (conf == NULL)
    {
        return NULL;
    }

    /*
     * set by ngx_pcalloc():
     *
     *     conf->types = { NULL };
     *     conf->types_keys = NULL;
     *     conf->trim = NULL;
     *     conf->js = NULL;
     *     conf->css = NULL;
     */

    return conf;
}


static char *
ngx_http_trim_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child)
{
    ngx_http_trim_loc_conf_t *prev = parent;
    ngx_http_trim_loc_conf_t *conf = child;

    if (ngx_http_merge_types(cf, &conf->types_keys, &conf->types,
                             &prev->types_keys, &prev->types,
                             ngx_http_html_default_types)
        != NGX_OK)
    {
        return NGX_CONF_ERROR;
    }

    if (conf->trim == NULL)
    {
        conf->trim = prev->trim;
    }

    if (conf->js == NULL)
    {
        conf->js = prev->js;
    }

    if (conf->css == NULL)
    {
        conf->css = prev->css;
    }

    return NGX_CONF_OK;
}


static ngx_int_t
ngx_http_trim_filter_init(ngx_conf_t *cf)
{
    ngx_http_next_header_filter = ngx_http_top_header_filter;
    ngx_http_top_header_filter  = ngx_http_trim_header_filter;

    ngx_http_next_body_filter = ngx_http_top_body_filter;
    ngx_http_top_body_filter  = ngx_http_trim_body_filter;

    return NGX_OK;
}
